package org.jflame.context.filemanager;

import org.jflame.commons.file.FileHelper;
import org.jflame.commons.model.Chars;
import org.jflame.commons.net.HttpHelper;
import org.jflame.commons.util.DateHelper;
import org.jflame.commons.util.IOHelper;
import org.jflame.commons.util.MapHelper;
import org.jflame.commons.util.StringHelper;
import org.jflame.commons.util.UrlHelper;
import org.jflame.context.filemanager.MinioFileManager.MinioCfg;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;

import io.minio.CopyObjectArgs;
import io.minio.CopySource;
import io.minio.DownloadObjectArgs;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.ObjectWriteArgs;
import io.minio.ObjectWriteResponse;
import io.minio.PostPolicy;
import io.minio.PutObjectArgs;
import io.minio.PutObjectArgs.Builder;
import io.minio.RemoveObjectsArgs;
import io.minio.Result;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;

/**
 * minio文件管理实现
 * 
 * @author charles.zhang
 */
public class MinioFileManager extends OssFileManager<MinioCfg> {

    private MinioClient minioClient = null;

    public MinioFileManager(MinioCfg cfg) {
        super(cfg);
        if (StringHelper.isEmpty(cfg.getEndpoint()) && StringHelper.isEmpty(cfg.getServer())) {
            throw new IllegalArgumentException("minio must configure endpoint or server");
        }
        init();
    }

    public MinioFileManager(String endpoint, String accessId, String accessSecret, String bucketName) {
        this(new MinioCfg(endpoint, accessId, accessSecret, bucketName));
    }

    public MinioFileManager(String endpoint, String accessId, String accessSecret, String bucketName, String region) {
        this(new MinioCfg(endpoint, accessId, accessSecret, bucketName, region));
    }

    private void init() {
        MinioCfg cfg = getConfig();
        if (StringHelper.isEmpty(cfg.getEndpoint())) {
            if (StringHelper.isNotEmpty(cfg.getServer())) {
                cfg.setEndpoint(cfg.getServer());
            }
        } else {
            if (StringHelper.isEmpty(cfg.getServer())) {
                cfg.setServer(cfg.getEndpoint());
            }
        }
        if (!isProtocolStarter(cfg.getServer())) {
            cfg.setServer(cfg.isEnableHttps() ? "https://" : "http://" + cfg.getServer());
        }
        if (!isProtocolStarter(cfg.getEndpoint())) {
            cfg.setEndpoint(cfg.isEnableHttps() ? "https://" : "http://" + cfg.getEndpoint());
        }

        io.minio.MinioClient.Builder builder = MinioClient.builder()
                .endpoint(cfg.getEndpoint())
                .credentials(cfg.getAccessId(), cfg.getAccessSecret());
        if (cfg.getRegion() != null) {
            builder.region(cfg.getRegion());
        }

        minioClient = builder.build();
        if (cfg.isEnableVirtualStyleEndpoint()) {
            minioClient.enableVirtualStyleEndpoint();
        }
        minioClient.setTimeout(
                cfg.getConnectionTimeout() > 0 ? cfg.connectionTimeoutInMillis() : TimeUnit.SECONDS.toMillis(10),
                TimeUnit.SECONDS.toMillis(30),
                cfg.getReadTimeout() > 0 ? cfg.readTimeoutInMillis() : TimeUnit.SECONDS.toMillis(30));
    }

    @Override
    public String save(File file, String saveDir, Map<String,String> fileMeta) throws FileAccessException {
        String fileId = FileHelper.mergePathToUnix(saveDir, file.getName());
        if (fileId.charAt(0) == Chars.SLASH) {
            fileId = fileId.substring(1);
        }
        Optional<String> mimeType = guessContentType(file.getName());
        Builder builder = PutObjectArgs.builder()
                .bucket(getCurrentBucket())
                .object(fileId)
                .userMetadata(fileMeta);
        if (mimeType.isPresent()) {
            builder.contentType(mimeType.get());
        }
        InputStream fileStream = null;
        ObjectWriteResponse resp;
        try {
            fileStream = new FileInputStream(file);
            resp = minioClient.putObject(builder.stream(fileStream, file.length(), -1)
                    .build());
            return resp.object();
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IllegalArgumentException | IOException e) {
            throw new FileAccessException("上传到minio失败", e);
        } finally {
            IOHelper.closeQuietly(fileStream);
        }
    }

    @Override
    public String save(InputStream fileStream, String filePath, Map<String,String> fileMeta, boolean replaceExisting)
            throws FileAccessException {
        String fileId;
        if (replaceExisting) {
            fileId = getFileId(filePath);
        } else {
            fileId = createNewFileId(StringHelper.substringBeforeLast(filePath, "/"), FileHelper.getFilename(filePath));
            if (fileId.charAt(0) == Chars.SLASH) {
                fileId = fileId.substring(1);
            }
        }
        Optional<String> mimeType = guessContentType(fileId);
        Builder builder = PutObjectArgs.builder()
                .bucket(getCurrentBucket())
                .object(fileId)
                .userMetadata(fileMeta);
        if (mimeType.isPresent()) {
            builder.contentType(mimeType.get());
        }
        int objsize = -1;
        ObjectWriteResponse resp;
        try {
            if (fileMeta != null && fileMeta.containsKey("Content-Length")) {
                objsize = Integer.parseInt(fileMeta.get("Content-Length"));
            } else {
                if (fileStream instanceof ByteArrayInputStream || fileStream instanceof FileInputStream
                        || fileStream instanceof BufferedInputStream) {
                    objsize = fileStream.available();
                    if (objsize == 0) {
                        objsize = -1;
                    }
                }
            }

            resp = minioClient.putObject(
                    builder.stream(fileStream, objsize, objsize == -1 ? ObjectWriteArgs.MIN_MULTIPART_SIZE : -1)
                            .build());
            return resp.object();
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IllegalArgumentException | IOException e) {
            throw new FileAccessException("上传到minio失败", e);
        } finally {
            IOHelper.closeQuietly(fileStream);
        }
    }

    @Override
    public byte[] readBytes(String filePath) throws FileAccessException {
        String key = getFileId(filePath);
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(getCurrentBucket())
                .object(key)
                .build();
        try (InputStream contentStream = minioClient.getObject(args)) {
            return IOHelper.readBytes(contentStream);
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IllegalArgumentException | IOException e) {
            throw new FileAccessException("minio文件读取异常:" + filePath, e);
        }
    }

    @Override
    public File read(String filePath) throws FileAccessException {
        String key = getFileId(filePath);
        Path tmpPath = mappingTmpFile(key);
        // 使用缓存的临时文件
        Optional<File> localCacheFileOpl = getTmpCacheFile(tmpPath);
        if (localCacheFileOpl.isPresent()) {
            return localCacheFileOpl.get();
        }
        try {
            DownloadObjectArgs args = DownloadObjectArgs.builder()
                    .bucket(getCurrentBucket())
                    .object(key)
                    .filename(tmpPath.toString())
                    .overwrite(true)
                    .build();
            minioClient.downloadObject(args);
            File tmpFile = tmpPath.toFile();
            return tmpFile;
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IOException e) {
            throw new FileAccessException("minio读取文件异常:" + filePath, e);
        }
    }

    @Override
    public InputStream readStream(String filePath) throws FileAccessException {
        String key = getFileId(filePath);
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(getCurrentBucket())
                .object(key)
                .build();
        try {
            return minioClient.getObject(args);
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IOException e) {
            throw new FileAccessException("minio读取文件异常:" + filePath, e);
        }
    }

    @Override
    public void download(String filePath, Path savePath) throws FileAccessException {
        String key = getFileId(filePath);
        try {
            DownloadObjectArgs args = DownloadObjectArgs.builder()
                    .bucket(getCurrentBucket())
                    .object(key)
                    .filename(savePath.toString())
                    .overwrite(true)
                    .build();
            minioClient.downloadObject(args);
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IOException e) {
            throw new FileAccessException("minio读取文件异常:" + filePath, e);
        }
    }

    @Override
    public int delete(String... fileKey) throws FileAccessException {
        return delete(getCurrentBucket(), fileKey);
    }

    @Override
    public int delete(String bucketName, String[] fileKeys) throws FileAccessException {
        if (ArrayUtils.isEmpty(fileKeys)) {
            throw new IllegalArgumentException();
        }
        if (logger.isInfoEnabled()) {
            logger.info("删除文件:{}", ArrayUtils.toString(fileKeys));
        }
        List<DeleteObject> delObjs = new ArrayList<>(fileKeys.length);
        for (int i = 0; i < fileKeys.length; i++) {
            DeleteObject dobj = new DeleteObject(getFileId(fileKeys[i]));
            delObjs.add(dobj);
        }
        RemoveObjectsArgs args = RemoveObjectsArgs.builder()
                .bucket(StringHelper.isEmpty(bucketName) ? getCurrentBucket() : bucketName)
                .objects(delObjs)
                .build();
        Iterable<Result<DeleteError>> it = minioClient.removeObjects(args);
        StringBuilder logBuilder = new StringBuilder(fileKeys.length * 20);
        int ei = 0;
        for (Result<DeleteError> r : it) {
            try {
                DeleteError err = r.get();
                logBuilder.append("del obj:")
                        .append(err.objectName())
                        .append(" errmsg:")
                        .append(err.message());
                ei++;
            } catch (InvalidKeyException | ErrorResponseException | IllegalArgumentException | InsufficientDataException
                    | InternalException | InvalidResponseException | NoSuchAlgorithmException | ServerException
                    | XmlParserException | IOException e) {
                logBuilder.append("\n delete ex:")
                        .append(e.getMessage());
                ei++;
            }
        }
        if (logBuilder.length() > 0) {
            logger.info(logBuilder.toString());
        }
        return fileKeys.length - ei;
    }

    @Override
    public String copy(String srcFilePath, String destFilePath) throws FileAccessException {
        String srckey = getFileId(srcFilePath);
        String destFile;
        String srcBucket = getCurrentBucket();
        String destBucket;
        if (StringHelper.isEmpty(destFilePath)) {
            int extBefore = srckey.lastIndexOf('.');
            if (extBefore < 1) {
                extBefore = srckey.length();
            }
            destFile = StringHelper.insertAt(srckey, String.format(copy_name_fmt, DateHelper.formatNow("yyMMddHHmm"),
                    RandomStringUtils.random(3, true, true)), extBefore);
            destBucket = srcBucket;
        } else {
            // String destkey = getFileId(destFilePath);
            if (destFilePath.startsWith("bucket:")) {
                destBucket = StringHelper.substringBetween(destFilePath, "bucket:", "/");
                destFilePath = StringHelper.substringAfter(destFilePath, "/");
            } else {
                destBucket = srcBucket;
            }
            // 复制到目录，文件名不变
            if (StringHelper.endChar(destFilePath) == Chars.SLASH) {
                destFile = destFilePath + FileHelper.getFilename(srckey);
            } else {
                destFile = destFilePath;
            }

        }
        CopyObjectArgs args = CopyObjectArgs.builder()
                .bucket(destBucket)
                .object(destFile)
                .source(CopySource.builder()
                        .bucket(srcBucket)
                        .object(srckey)
                        .build())
                .build();
        try {
            minioClient.copyObject(args);
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IOException e) {
            throw new FileAccessException("minio复制文件异常" + srcFilePath, e);
        }
        return destFile;
    }

    @Override
    public SignedUrl generatePresignedPutUrl(PresignedUrlArgs args) {
        return generatePresignedUrl(Method.PUT, args);
    }

    @Override
    public String generatePresignedGetUrl(PresignedUrlArgs args) {
        return generatePresignedUrl(Method.GET, args).urlStr();
    }

    private SignedUrl generatePresignedUrl(Method method, PresignedUrlArgs args) {
        String fileId;
        Map<String,String> clientReqHeader = new HashMap<>();
        if (method == Method.GET || method == Method.DELETE) {
            fileId = getFileId(args.getFileNameOrExt());
        } else {
            fileId = createNewFileId(args.getSavePath(), args.getFileNameOrExt());
            Map<String,String> headers = setContentType(fileId, args.getHeaders());
            if (MapHelper.isNotEmpty(headers)) {
                clientReqHeader.putAll(headers);
            }
            // 添加用户自定义元信息。
            /*if (MapHelper.isNotEmpty(userMetadata)) {
                clientReqHeader.putAll(userMetadata);
            }*/
        }
        try {
            String presignedUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(method)
                    .bucket(getCurrentBucket())
                    .object(fileId)
                    .expiry(args.getExpireTime(), TimeUnit.SECONDS)
                    .extraHeaders(args.getHeaders())
                    .extraQueryParams(args.getQueryParams())
                    .build());
            return new SignedUrl(URI.create(presignedUrl)
                    .toURL(), clientReqHeader);
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | XmlParserException | ServerException
                | IllegalArgumentException | IOException e) {
            throw new RuntimeException("生成minio预签名上传地址失败", e);
        }
    }

    @Override
    public Map<String,String> generatePostObjectParams(PresignedUrlArgs args) {
        String fileId = createNewFileId(args.getSavePath(), args.getFileNameOrExt());
        Optional<String> mimeType = guessContentType(fileId);// 图片加contenttype

        PostPolicy policy = new PostPolicy(getCurrentBucket(), ZonedDateTime.now()
                .plusSeconds(args.getExpireTime()));
        policy.addEqualsCondition("key", fileId);
        if (mimeType.isPresent()) {
            policy.addStartsWithCondition(HttpHelper.HEADER_CONTENTTYPE, mimeType.get());
        }
        policy.addContentLengthRangeCondition(0, args.getMaxFileSize());

        try {
            Map<String,String> formData = minioClient.getPresignedPostFormData(policy);
            formData.put("key", fileId);
            formData.put("host", getServerUrl() + "/" + getCurrentBucket());
            // bug,文档说content-type不需要出现在表单,实际是需要,否则会报错
            if (mimeType.isPresent()) {
                formData.put(HttpHelper.HEADER_CONTENTTYPE, mimeType.get());
            }
            return formData;

        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IOException e) {
            throw new RuntimeException("生成postObject上传参数异常", e);
        }

    }

    @Override
    public List<String> listFiles(String dir, int maxFiles, boolean isExcludeDir) throws FileAccessException {
        if (maxFiles <= 0) {
            maxFiles = 1000;// 最大数
        }
        int dirLen = dir != null ? dir.length() : 0;
        if (dirLen > 0 && !"/".equals(dir)) {
            if (StringHelper.endChar(dir) != Chars.SLASH) {
                dir = dir + Chars.SLASH;
                dirLen += 1;
            }
        }
        ListObjectsArgs args = ListObjectsArgs.builder()
                .bucket(getCurrentBucket())
                .recursive(false)
                .maxKeys(maxFiles)
                .fetchOwner(false)
                .prefix(dir)
                .build();

        List<String> filenames = new ArrayList<>(maxFiles);
        String filename;
        Iterable<Result<Item>> listing;
        try {
            listing = minioClient.listObjects(args);
            for (Result<Item> objSummary : listing) {
                if (isExcludeDir && objSummary.get()
                        .isDir()) {
                    continue;
                }
                filename = objSummary.get()
                        .objectName();
                filenames.add(filename);
            }
        } catch (XmlParserException | InvalidKeyException | ErrorResponseException | IllegalArgumentException
                | InsufficientDataException | InternalException | InvalidResponseException | NoSuchAlgorithmException
                | ServerException | IOException e) {
            throw new FileAccessException("minio列举目录文件异常:" + dir, e);
        }

        return filenames;
    }

    @Override
    public void close() throws IOException {
        minioClient = null;
    }

    public MinioClient getClient() {
        return minioClient;
    }

    @Override
    public void createBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .region(getConfig().getRegion())
                    .build());
        } catch (InvalidKeyException | ErrorResponseException | InsufficientDataException | InternalException
                | InvalidResponseException | NoSuchAlgorithmException | ServerException | XmlParserException
                | IllegalArgumentException | IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toRequestUrl(String filePath) {
        if (getServerUrl() != null && !isProtocolStarter(filePath)) {
            return UrlHelper.mergeUrl(getServerUrl(), getCurrentBucket(), filePath);
        } else {
            return filePath;
        }
    }

    public static class MinioCfg extends OssManagerCfg {

        private boolean enableVirtualStyleEndpoint = false;
        /**
         * minio地址
         */
        private String endpoint;

        public MinioCfg() {
        }

        public MinioCfg(String server, String accessId, String accessSecret, String bucketName, String region) {
            super(server, accessId, accessSecret, bucketName, region);
        }

        public MinioCfg(String server, String accessId, String accessSecret, String bucketName) {
            super(server, accessId, accessSecret, bucketName);
        }

        public boolean isEnableVirtualStyleEndpoint() {
            return enableVirtualStyleEndpoint;
        }

        public void setEnableVirtualStyleEndpoint(boolean enableVirtualStyleEndpoint) {
            this.enableVirtualStyleEndpoint = enableVirtualStyleEndpoint;
        }

        public String getEndpoint() {
            return endpoint;
        }

        public void setEndpoint(String endpoint) {
            this.endpoint = endpoint;
        }

    }

}
